fix(Android): RTL layout for center-aligned custom header title#3896
fix(Android): RTL layout for center-aligned custom header title#3896HakimMohamed wants to merge 3 commits intosoftware-mansion:mainfrom
Conversation
The shadow tree expects physical left/right padding, but the toolbar APIs return logical start/end values which swap sides in RTL. This caused the center-aligned custom header title (headerTitle as function) to receive near-zero width in RTL, making it invisible. Fixes software-mansion#3438
kkafar
left a comment
There was a problem hiding this comment.
Hey, thanks for the PR!
I'm not convinced this is the right fix yet.
Yoga is RTL aware & it should swap the paddings on its own, when the layout direction changes. I'll try to investigate it a bit deeper soon.
| val isRtl = toolbar.layoutDirection == LAYOUT_DIRECTION_RTL | ||
|
|
||
| val contentInsetStartEstimation = | ||
| val startInsetEstimation = |
There was a problem hiding this comment.
Unnecessary renaming. This value name should stay untouched.
There was a problem hiding this comment.
Good point dug into it and Yoga does handle logical edges correctly, but the existing setPadding() call maps paddingStart/paddingEnd to physical Edge::Left/Edge::Right, so the values never reach Yoga as logical. Pushed a commit that routes them through Edge::Start/Edge::End instead; tested in FabricExample and my production app, renders correctly in LTR and RTL.
There was a problem hiding this comment.
Pull request overview
Fixes an Android RTL rendering issue where a center-aligned custom headerTitle (function component) could become invisible due to start/end insets being interpreted as physical left/right padding in the Fabric shadow tree.
Changes:
- Adjust Android header config layout reporting to account for RTL by mapping logical start/end insets to physical left/right padding.
- Add a new issue-test (
Test3438) and export it from the issue-tests index. - Update header layout inset calculations used to size/position header subviews in Fabric.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| apps/src/tests/issue-tests/index.ts | Exports the new manual reproduction test for issue #3438. |
| apps/src/tests/issue-tests/Test3438.tsx | Adds a manual reproduction case for centered custom header title in RTL. |
| android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt | Updates inset-to-padding mapping to avoid RTL start/end swap issues when sending state to the Fabric shadow tree. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else { | ||
| paddingLeft = endInset | ||
| paddingRight = | ||
| if (leftSubview != null) toolbar.width - leftSubview.left else startInsetEstimation |
There was a problem hiding this comment.
In RTL, toolbar.width - leftSubview.left overestimates the physical right padding because it includes the left-subview’s width (it measures from the subview’s left edge, not its right edge). This can still collapse the available width for the centered title. Compute the right-side inset from the toolbar’s right edge to the subview’s right edge instead (or otherwise subtract the subview width).
| if (leftSubview != null) toolbar.width - leftSubview.left else startInsetEstimation | |
| if (leftSubview != null) toolbar.width - leftSubview.right else startInsetEstimation |
| val isBackButtonDisplayed = toolbar.navigationIcon != null | ||
| val isRtl = toolbar.layoutDirection == LAYOUT_DIRECTION_RTL | ||
|
|
There was a problem hiding this comment.
LAYOUT_DIRECTION_RTL / LAYOUT_DIRECTION_LTR are used unqualified in this file, but there is no import for these constants. This won’t compile unless they’re referenced via View.LAYOUT_DIRECTION_* or imported explicitly.
| function App() { | ||
| return ( | ||
| <NavigationContainer> | ||
| <Stack.Navigator> | ||
| <Stack.Screen | ||
| name="Home" | ||
| component={HomeScreen} | ||
| options={{ | ||
| headerTitleAlign: 'center', | ||
| headerTitle: () => ( | ||
| <Text style={{ fontSize: 18, fontWeight: 'bold' }}> | ||
| Custom Title | ||
| </Text> | ||
| ), | ||
| }} | ||
| /> | ||
| </Stack.Navigator> |
There was a problem hiding this comment.
This reproduction screen is the root of the stack, so Android typically won’t show a back button/navigation icon. If the original issue depends on the start inset created by the back button, this example may not trigger the bug. Consider adding a second screen and navigating to it so the header renders with a back button (and/or add a headerRight) to reliably reproduce #3438 in RTL.
Previously paddingStart/paddingEnd were sent to setPadding() which maps to physical Edge::Left/Right — so the values were treated as physical, not logical, and Yoga never swapped them in RTL. Introduce setLogicalPadding() on the shadow node that sets padding on Edge::Start/End, letting Yoga handle RTL direction on its own. The Kotlin side now only converts leftSubview.left (physical) to a logical start-side distance in RTL; the rest is physical-free.
Description
When
headerTitleAlignis'center'andheaderTitleis a function (custom React component), the header title is invisible on Android in RTL mode. LTR works fine, iOS works fine.Root cause:
onNativeToolbarLayoutsendspaddingStart/paddingEnd(logical, direction-aware) to the C++ shadow tree, which uses them as physicalleft/rightYoga padding. In RTL, start=right and end=left, so the paddings were swapped — a large right-side value ended up as left padding, leaving near-zero width for the centered title.Fixes #3438
Changes
toolbar.layoutDirectioninonNativeToolbarLayoutTest3438.tsxreproducing the issueTest plan
Test3438in FabricExampleChecklist